﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using com.bodurov.SessionRecovery.Interfaces;

namespace com.bodurov.SessionRecovery
{
    public class SessionRecoveryStorageManager : ISessionRecoveryStorageManager
    {
        private readonly ISessionRecoveryConfiguration _config;
        private readonly ISessionRecoverySerializer _serializer;
        private readonly ISessionRecoveryLogger _logger;

        private readonly HashSet<string> _sessionIDs = new HashSet<string>();
        private bool _updateActiveSessionsHasBeenCalled;

        private static readonly object _synchRoot1 = new object();
        private static readonly object _synchRoot2 = new object();

        public SessionRecoveryStorageManager(
                    ISessionRecoveryConfiguration config, 
                    ISessionRecoverySerializer serializer,
                    ISessionRecoveryLogger logger)
        {
            this._config = config;
            this._serializer = serializer;
            this._logger = logger;
        }



        public void Save(string sessionID, Hashtable hashtable)
        {
            if (!this._updateActiveSessionsHasBeenCalled)
            {
                this.UpdateActiveSessions();
            }
            // here multiple threads should be ok because they will all have different session IDs
            this._sessionIDs.Add(sessionID);
            var fileName = this.GetFileName(sessionID);
            this._logger.Log(TraceLevel.Info, "Creating file '{0}' thread ID = '{1}'", fileName, Thread.CurrentThread.ManagedThreadId);
            using (var stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite))
            {
                this._serializer.Serialize(stream, hashtable);
                stream.Close();
            }
        }

        public Hashtable Recover(string sessionID)
        {
            if (!this._updateActiveSessionsHasBeenCalled)
            {
                this.UpdateActiveSessions();
            }
            Hashtable deserialized = null;
            lock (_synchRoot2)
            {
                var fileName = this.GetExistingFileBySessionID(sessionID);
                if (fileName != null)
                {
                    this._logger.Log(TraceLevel.Info, "Reading file '{0}' thread ID = '{1}'", fileName, Thread.CurrentThread.ManagedThreadId);
                    try
                    {
                        using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                        {
                            deserialized = this._serializer.Deserialize(stream);
                            stream.Close();
                        }
                        this.DeleteFileAndRemoveSession(fileName, sessionID);
                    }catch(Exception)
                    {
                        // If there is a problem deserializing the data
                        // delete the saved session data and remove the session.
                        // In this case the result is just as if the tool was not 
                        // installed - the session is lost, later in the call stack
                        // the log will capture the reason for that failure.
                        this.DeleteFileAndRemoveSession(fileName, sessionID);
                        throw;
                    }
                }
            }
            return deserialized;
        }

        private void DeleteFileAndRemoveSession(string fileName, string sessionID)
        {
            File.Delete(fileName);
            this._sessionIDs.Remove(sessionID);
        }

        public int NumberActiveSessions
        {
            get { return this._sessionIDs.Count; }
        }

        public void EnsureActiveSessions()
        {
            if (this.NumberActiveSessions == 0)
            {
                this.UpdateActiveSessions();
            }
        }

        public bool ContainsActiveSession(string sessionID)
        {
            return this._sessionIDs.Contains(sessionID);
        }
        public void ManagerIsEnabled()
        {
            UpdateActiveSessions();
        }
        public void ManagerIsDisabled()
        {
            UpdateActiveSessions();
            this._updateActiveSessionsHasBeenCalled = false;
        }

        private string GetExistingFileBySessionID(string sessionID)
        {
            var dir = new DirectoryInfo(this._config.SessionFilesFolder);
            foreach (var file in dir.GetFiles())
            {
                if (!this._config.FileIsSessionFile(file.Name))
                {
                    continue;
                }
                var currSessID = this._config.GetSessionIdByFileName(file.Name);
                if(currSessID == sessionID)
                {
                    return file.FullName;
                }
            }
            return null;
        }

        /// <summary>
        /// Delete expired session files and update _sessionIDs collection
        /// </summary>
        private void UpdateActiveSessions()
        {
            var dir = new DirectoryInfo(this._config.SessionFilesFolder);
            // normally this method should not be called by two threads 
            // but just in case there is a design mistake we lock it
            lock (_synchRoot1)
            {
                foreach(var file in dir.GetFiles())
                {
                    if(!this._config.FileIsSessionFile(file.Name))
                    {
                        continue;
                    }
                    var fileDate = this._config.GetDateTimeByFileName(file.Name);
                    var sessID = this._config.GetSessionIdByFileName(file.Name);
                    var span = DateTime.UtcNow - fileDate;
                    if (span.TotalMinutes > this._config.NumMinutesAfterWhichRestorIsNotAllowed)
                    {
                        file.Delete();
                        this._sessionIDs.Remove(sessID);
                    }
                    else
                    {
                        this._sessionIDs.Add(sessID);
                    }
                }

                this._updateActiveSessionsHasBeenCalled = true;
            }
        }

        private string GetFileName(string sessionID)
        {
            return this._config.GetFileName(sessionID);
        }
    }
}
